/**
 * Copyright (c) 2023 murenchao
 * taomu is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *       http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */
package cool.taomu.utils

import com.google.common.collect.HashBasedTable
import com.google.common.collect.Table
import java.lang.invoke.LambdaMetafactory
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import org.eclipse.xtend.lib.annotations.Accessors

class CallSiteUtils {
	@Accessors
	static class Mapping {
		MethodHandle handle;
		Class<?>[] parameterTypes;
		Class<?> implZlass;
	}

	Class<?> inter;
	val static Table<String, String, Mapping> providers = HashBasedTable.create();

	new(Class<?> inter) {
		this.inter = inter;
	}

	def bind(Class<?> src) {
		inter.declaredMethods.forEach [ i |
			src.declaredMethods.forEach [ s |
				if (i.parameterTypes.immutableCopy.equals(s.parameterTypes.immutableCopy)) {
					var lookup = MethodHandles.lookup();
					var mh = lookup.unreflect(s);
					var type = mh.type;
					var factoryType = MethodType.methodType(inter, type.parameterType(0));
					type = type.dropParameterTypes(0, 1);
					var lam = LambdaMetafactory.metafactory(lookup, i.name, factoryType, type, mh, type).target;
					var mapping = new Mapping();
					mapping.handle = lam;
					mapping.implZlass = src;
					mapping.parameterTypes = type.parameterArray;
					providers.put(i.name + mapping.parameterTypes.immutableCopy, s.name, mapping);
				}
			]
		]
		return this;
	}

	def bind(Class<?> src, Object... args) {
		var String methodName = "";
		try {
			var finterface = inter.getAnnotation(FunctionalInterface);
			val method = inter.declaredMethods.get(0);
			methodName = method.name;
			if (finterface !== null) {
				var bindMethod = src.declaredMethods.findFirst [
					it.parameterTypes.immutableCopy.equals(method.parameterTypes.immutableCopy)
				]
				var lookup = MethodHandles.lookup();
				var mh = lookup.unreflect(bindMethod);
				var type = mh.type;
				var factoryType = MethodType.methodType(inter, type.parameterType(0));
				type = type.dropParameterTypes(0, 1);
				var handle = LambdaMetafactory.metafactory(lookup, method.name, factoryType, type, mh, type).target;
				var instance = handle.invoke(src.getConstructor().newInstance());
				return method.invoke(instance, args);
			}
		} catch (Exception e) {
			throw new CodecGenerationException("Could not generate the function to access the bind " + methodName, e);
		}
	}

	def invoke(String methodName, Object ... args) {
		var argTypes = args.immutableCopy.map[it.class];
		var s = providers.get(methodName + argTypes, methodName);
		var i = inter.getDeclaredMethod(methodName, argTypes)
		var o = s.implZlass.getConstructor().newInstance();
		var instance = s.handle.invoke(o);
		return i.invoke(instance, args);
	}
}

class CodecGenerationException extends Exception {

	new(String message, Throwable object) {
		super(message, object);
	}

}
